/*
 *  Blazon: A user datagram protocol re-braodcasting daemon.
 *  io.c: Network socket and packet processing.
 *
 *  Copyright 2012 Blazon development team
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "std-include.h"

#include <sys/types.h>		/* socket() and PF_ROUTE, inet_ntop() */
#include <sys/time.h>		/* socket() and PF_ROUTE stuff */
#include <sys/socket.h>		/* socket() and PF_ROUTE stuff */

#include <netinet/in.h>	/* inet_ntop() */
#include <arpa/inet.h>	/* inet_ntop() */
#include <netinet/ip.h>	/* IP headers */
#include <netinet/udp.h>	/* UDP headers */
#include <fcntl.h>		/* non-blocking call */

#include "tools.h"
#include "base-io.h"
#include "hash.h"
#include "match.h"
#include "config.h"
#include "blazon.h"
#include "io.h"
#include "log.h"

static int rawfd = -1;

struct pseudo_udp {
	struct in_addr src;
	struct in_addr dst;
	unsigned char pad;
	unsigned char proto;
	unsigned short udp_len;
	struct udphdr udp;
};


/* Function declarations */
void setup_sock_raw(void);
void close_sock_raw(void);
void setup_game_sockets(struct config_file *);
void close_game_sockets(dlink_list game_list);
void update_game_sockets(struct config_file *, struct config_file *);

void read_from_socket(fdext_t *, struct subnet_info *,
		int, struct hashtable *);
static int send_udp_packet(struct sockaddr_in *, struct game_info *,
		struct subnet_info *, const char *, int);

static uint16_t ip_checksum(uint16_t *, int);
static uint16_t udp_checksum(uint32_t, uint32_t, uint16_t *, int);


void
setup_sock_raw(void)
{
	int flags;

	rawfd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);

	if (rawfd < 0)
		die("Failed to initialise SOCK_RAW");

	flags = 1;		/* 1 = on, 0 = off */
	if (setsockopt(rawfd, IPPROTO_IP, IP_HDRINCL, &flags, sizeof(flags)) < 0)
		die("Failed to setsockopt()");

	/* set the socket to be non-blocking */
	flags = fcntl(rawfd, F_GETFL, 0);
	flags |= O_NONBLOCK;
	if (fcntl(rawfd, F_SETFL, flags) == -1)
		die("Setting non-block for sock_raw");
}


void
close_sock_raw(void)
{
	if (rawfd > -1) {
		close(rawfd);
		rawfd = -1;
	}
}


void
setup_game_sockets(struct config_file *conf)
{
	struct game_info *game_ptr;
	dlink_node *ptr;
	fdext_t *F;

	DLINK_FOREACH(ptr, conf->game_list.head)
	{
		game_ptr = ptr->data;

		/* This should check whether a game is already set up. */
		if (game_ptr->fd_ptr != NULL)
		{
			log_debug("Socket for '%s' is open.", game_ptr->name);
		}
		else
		{
			F = bind_udp_socket(conf->listen, game_ptr->port);

			if (F != NULL)
			{
				game_ptr->fd_ptr = F;
				log_debug("Bound socket for '%s', port %d",
						game_ptr->name, game_ptr->port);
				/* Add the fdext_t -> game_info pointer. */
				F->data = game_ptr;
				if (F->descr == NULL)
					F->descr = my_strndup(game_ptr->name, FDE_DESCR_SIZE);
			}
			else
				log_err("Cannot bind() for address %s, port %d",
						conf->listen, game_ptr->port);
		}
	}


	/*(*conf).games[i].fd_ptr = bind_udp_socket(conf->listen, conf->games[i].port);*/

	/* games[i].fd is now an fdext_t ptr
	if(conf->games[i].fd < 0)
		die("Initialising socket for port %d", conf->games[i].port);*/

}


void
close_game_sockets(dlink_list game_list)
{
	struct game_info *game_ptr;
	dlink_node *ptr;

	DLINK_FOREACH(ptr, game_list.head)
	{
		game_ptr = ptr->data;

		if (game_ptr->fd_ptr != NULL)
		{
			log_info("Closed socket for '%s' (%d)",
					game_ptr->name, game_ptr->port);
			fdext_close(game_ptr->fd_ptr);
			game_ptr->fd_ptr = NULL;
		}
	}
}


/* update_game_sockets() - Compare the new and running config to see
 * which (if any) game sockets need to be closed. At the same time,
 * copy the socket descriptors across for use by the new config.
 */
void
update_game_sockets(struct config_file *conf, struct config_file *newconf)
{
	struct game_info *game_ptr, *ngame_ptr;
	dlink_node *ptr, *nptr;
	bool close_fd;

	log_info("Updating game sockets.");

	DLINK_FOREACH(ptr, conf->game_list.head)
	{
		close_fd = true;

		DLINK_FOREACH(nptr, newconf->game_list.head)
		{
			game_ptr = ptr->data;
			ngame_ptr = nptr->data;

			if (ngame_ptr->port == game_ptr->port)
			{
				ngame_ptr->fd_ptr = game_ptr->fd_ptr;
				ngame_ptr->sentcount = game_ptr->sentcount;
				ngame_ptr->throtcount = game_ptr->throtcount;
				close_fd = false;
				/* Update the fdext_t -> game_info pointer. */
				ngame_ptr->fd_ptr->data = ngame_ptr;
				break;
			}
		}

		if (close_fd)
		{
			log_info("Closed socket for '%s' (%d)",
					game_ptr->name, game_ptr->port);
			fdext_close(game_ptr->fd_ptr);
			game_ptr->fd_ptr = NULL;
		}
	}
}


static void
udp_replicator(struct sockaddr_in src_addr, char *ipstr,
		struct game_info *game, const char *load, int pkt_len,
		struct subnet_info *subnets, int subnets_size)
{
	int i, count;

	/* Match the packet against our subnets. */
	count = 0;
	for(i = 0; i < subnets_size; i++)
	{
		if(match_ip(ipstr, subnets[i].subnet))
		{
			/*log_info("%s:%d %d bytes => not sent to %s",
				ipstr, game->port, nbytes, subnets[i].subnet);*/
		}
		else
		{

			send_udp_packet(&src_addr, game, &subnets[i], load, pkt_len);
			count++;
		}
	}
	(*game).sentcount++;

	log_info("%s:%s => sent to %d networks [sentcount: %d]",
			ipstr, game->name, count, game->sentcount);
}


void
read_from_socket(fdext_t *F, struct subnet_info *subnets,
		int subnets_size, struct hashtable *hasht)
{
	int nbytes, i, rcycle = 0;
	struct sockaddr_in src_addr;
	socklen_t srclen;
	char buf[BUFLEN];
	char ipstr[INET_ADDRSTRLEN+1];
	char hashbuf[25+1];
	struct hashpkt *pkt_p;
	bool length_ok;

	if (F == NULL)
	{
		die("read_from_socket() F is NULL");
		return;
	}
	if (F->data == NULL)
	{
		die("read_from_socket() F->data is NULL");
		return;
	}

	struct game_info *game = F->data;

	do
	{
		/* Fetch the packet msg data from the socket. */
		/*nbytes = fetch_recvmsg_data(game->fd, &src_addr, buf);*/
		/*nbytes = recvfrom(sockfd, buf, sizeof buf, 0, &addr, &fromlen);*/
		srclen = sizeof(src_addr);
		nbytes = recvfrom(F->fd, buf, BUFLEN, 0,
				(struct sockaddr *)&src_addr, &srclen);
		/*log_err("read %d bytes from socket", nbytes);
		log_err("skt payload: %c,%c,%c,%c,%c", buf[0], buf[1], buf[2], buf[3], buf[4]);*/

		if (nbytes < 1)
			break;

		inet_ntop(src_addr.sin_family, &src_addr.sin_addr, ipstr, INET_ADDRSTRLEN);
		/*inet_ntop(src_addr.ss_family,
				src_addr.ss_family == AF_INET ?
					((struct sockadd_in *)&src_addr)->sin_addr :
					((struct sockadd_in6 *)&src_addr)->sin6_addr,
				ipstr, INET_ADDRSTRLEN);*/


		/* Check the UDP payload has the length we're expecting.
		 * Omit packet length checking if the first length is 0.
		 */
		if (game->lens[0] == 0)
			length_ok = true;
		else
		{
			length_ok = false;
			for(i = 0; i < game->lens_size; i++)
			{
				if (nbytes == game->lens[i])
					length_ok = true;
			}
		}


		if (!length_ok)
		{
			log_warn("%s:%s, %d bytes => dropped (bad length)",
					ipstr, game->name, nbytes);

			continue;
		}

		/* Apply the packet send frequency limiting. */
		snprintf(hashbuf, sizeof(hashbuf), "%s:%d", ipstr, game->port);
		pkt_p = hashnode_find(hasht, hashbuf);

		time_t delta;

		if(pkt_p)
		{
			delta = CURRENT_TIME - pkt_p->timestamp;
			if(delta < 3)
			{
				log_info("%s:%s, %d bytes => rate-limited (%d secs)",
						ipstr, game->name, nbytes, delta);
				(*game).throtcount++;
				continue;
			}
		}

		/* Replicate UDP to all valid subnets. */
		udp_replicator(src_addr, ipstr, game, buf, nbytes,
				subnets, subnets_size);


		/*pkt_p = NULL;*/

		if(!pkt_p)
		{
			/*log_info("Allocating memory for node data (pkt)");*/
			pkt_p = my_malloc(sizeof(struct hashpkt));
			/*log_info("Setting new time for new node data %d", CURRENT_TIME);*/
			pkt_p->timestamp = CURRENT_TIME;
			/*log_info("Adding new node to hash_table");*/
			hashnode_add(hasht, hashbuf, pkt_p);
		}
		else
		{
			/*log_info("Updating time for existing node data %d", CURRENT_TIME);*/
			pkt_p->timestamp = CURRENT_TIME;
		}


		rcycle++;

	} while (nbytes > 0 && rcycle < MAX_READS);

}


static int
send_udp_packet(struct sockaddr_in *src_addr, struct game_info *game,
		struct subnet_info *subnet, const char *load, int pkt_len)
{
	struct ip *ip;
	struct udphdr *udp;
	char *pload;
	char buf[1024];
	struct sockaddr_in dest_addr;
	int sent;

	ip = (struct ip *) buf;
	udp = (struct udphdr *) (buf + sizeof(struct ip));
	pload = (char *) (buf + sizeof(struct ip) + sizeof(struct udphdr));

	memset(buf, 0, 1024);
	memset(&dest_addr, 0, sizeof(struct sockaddr_in));

	dest_addr.sin_family = AF_INET;
	dest_addr.sin_port = htons(game->port);
	inet_pton(AF_INET, subnet->broadcast, &dest_addr.sin_addr.s_addr);

	ip->ip_v = IPVERSION;						/* version (4 bits) */
	ip->ip_hl = (sizeof(struct ip) >> 2);		/* header length (4 bits) */
	ip->ip_len = sizeof(struct ip) + sizeof(struct udphdr) + pkt_len;	/* total length */
	ip->ip_id = 0;								/* 0 means kernel sets value */

	ip->ip_tos = IPTOS_LOWDELAY;				/* type of service */
	ip->ip_off = 0;								/* no fragment */
	ip->ip_ttl = 64;							/* default value */
	ip->ip_p = IPPROTO_UDP;						/* protocol at L4 */
	ip->ip_src = src_addr->sin_addr;
	ip->ip_dst = dest_addr.sin_addr;
	/* IP checksum field */
	ip->ip_sum = 0;
	ip->ip_sum = ip_checksum( (uint16_t *) ip, sizeof(struct ip) );

	/* Set up the UDP header. */
	udp->uh_sport = src_addr->sin_port;
	udp->uh_dport = htons(game->port);
	udp->uh_ulen = htons(sizeof(struct udphdr) + pkt_len);

	/* Copy our UDP payload */
	memcpy(pload, load, pkt_len);

	/* UDP checksum */
	udp->uh_sum = 0;
	udp->uh_sum = udp_checksum(ip->ip_src.s_addr, ip->ip_dst.s_addr,
							(uint16_t *) udp, sizeof(struct udphdr) + pkt_len);


	sent = sendto(rawfd, buf, ip->ip_len, 0, (struct sockaddr *)&dest_addr,
				sizeof(dest_addr));
	if(sent < ip->ip_len)
	{
		if(sent < 1)
			log_err("sendto() error: %s", strerror(errno));
		else
			log_err("sendto() incomplete (%d bytes remain)",
					(ip->ip_len - sent));
	}

	return 0;
}


static uint16_t
ip_checksum(uint16_t *buf, int nwords)
{
	uint32_t sum;
	uint16_t *word = buf;

	for(sum = 0; nwords > 0; nwords--) /* add words(16bits) together */
		sum += *word++;
	sum = (sum >> 16) + (sum & 0xffff); /* add carry over */
	sum += (sum >> 16); /* condenses sum (32-bit) into 16-bit checksum */

	return (uint16_t)(~sum);
}


static uint16_t
udp_checksum(uint32_t src, uint32_t dst, uint16_t *udp_hdr, int len)
{
	char buf[1024];
	struct pseudo_udp *pseudo_hdr;

	pseudo_hdr = (struct pseudo_udp *) buf;

	memset(buf, 0, 1024);

	pseudo_hdr->src.s_addr = src;
	pseudo_hdr->dst.s_addr = dst;
	pseudo_hdr->pad = 0;
	pseudo_hdr->proto = IPPROTO_UDP;
	pseudo_hdr->udp_len = htons(len);

	memcpy(&(pseudo_hdr->udp), udp_hdr, len);

	/* Check if the legnth is odd. */
	if((len & 1) != 0)
		len++;

	return ip_checksum((uint16_t *)buf, 12 + len);
}

